package com.qiaofang.applet.common.client.weixin;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.qiaofang.applet.common.client.weixin.model.WxAuthResponse;
import com.qiaofang.applet.common.client.weixin.model.WxDecryptPhone;
import com.qiaofang.applet.common.client.weixin.model.WxProperties;
import com.qiaofang.applet.common.client.weixin.model.WxToken;
import com.qiaofang.applet.common.exception.BusinessException;
import com.qiaofang.applet.common.util.RedisKeyUtil;
import com.qiaofang.applet.common.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.spec.AlgorithmParameterSpec;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * @author junze.liu
 */
@Slf4j
@Service
public class WxService {

    private RestTemplate restTemplate = new RestTemplate();

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 获取小程序 accessToken
     *
     * @param config
     * @return
     */
    public String getAccessToken(WxProperties config) {
        //从redis中获取accessToken
        Object obj = redisUtil.get(RedisKeyUtil.getAccessTokenKey(config.getAppId()));
        //获取到时，返回accessToken
        if (obj != null) {
            return obj.toString();
        }
        //未获取到时，调用微信api获取accessToken
        Map<String, Object> paramsMap = new HashMap<>(3);
        paramsMap.put(WxConfig.GRANT_TYPE, WxConfig.GRANT_TYPE_VALUE1);
        paramsMap.put(WxConfig.APP_ID, config.getAppId());
        paramsMap.put(WxConfig.APP_SECRET, config.getAppSecret());
        WxToken wxToken = restTemplate.getForObject(buildGetUrl(WxConfig.ACCESS_TOKEN_URI, paramsMap), WxToken.class);
        if (!StringUtils.isEmpty(wxToken.getAccess_token())) {
            redisUtil.set(RedisKeyUtil.getAccessTokenKey(config.getAppId()), wxToken.getAccess_token(), 7000);
        } else {
            log.error("获取accessToken失败", JSON.toJSONString(wxToken));
            throw new BusinessException("获取accessToken失败");
        }
        return wxToken.getAccess_token();
    }

    /**
     * 登录凭证校验, 获取小程序用户Openid
     *
     * @param code
     * @param config
     * @return
     */
    public WxAuthResponse getWxAuth(String code, WxProperties config) {
        Map<String, Object> paramsMap = new HashMap<>();
        paramsMap.put(WxConfig.APP_ID, config.getAppId());
        paramsMap.put(WxConfig.APP_SECRET, config.getAppSecret());
        paramsMap.put(WxConfig.JS_CODE, code);
        paramsMap.put(WxConfig.GRANT_TYPE, WxConfig.AUTHORIZATION_CODE);
        String result = restTemplate.getForObject(buildGetUrl(WxConfig.JS_CODE_URI, paramsMap), String.class);
        log.info("getWxAuth：{}", result);
        WxAuthResponse wxAuth = JSONObject.parseObject(result, WxAuthResponse.class);
        if (wxAuth.getErrcode() != 0) {
            log.error("获取小程序用户Openid失败, getWxAuth: {}", result);
            throw new BusinessException("获取小程序用户Openid失败, " + result);
        }
        //由于SessionKey在微信注册获取手机号时需要用到，所以缓存7000毫秒，如有需要可以存到数据库中
        redisUtil.set(RedisKeyUtil.getWxaSessionKey(wxAuth.getOpenid()), wxAuth.getSessionKey(), 7000);
        return wxAuth;
    }

    /**
     * 解析手机号码
     *
     * @param dp
     * @return
     */
    public String decryptPhone(WxDecryptPhone dp) {
        try {
            //从redis中获取sessionKey
            Object obj = redisUtil.get(RedisKeyUtil.getWxaSessionKey(dp.getOpenId()));
            //获取到时，返回accessToken
            if (obj == null) {
                log.error("decryptPhone ==> 未获取到sessionKey");
                return null;
            }
            AlgorithmParameterSpec ivSpec = new IvParameterSpec(dp.getIv());
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(Base64.decodeBase64(obj.toString()), "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            String str = new String(cipher.doFinal(dp.getEncryptedData()), "UTF-8");
            JSONObject jsonObject = JSONObject.parseObject(str);
            return jsonObject.getString("phoneNumber");
        } catch (Exception e) {
            log.error("微信小程序解析手机号码失败: ", e);
        }
        return null;
    }

    /**
     * 获取小程序码，适用于需要的码数量极多的业务场景。
     * 通过该接口生成的小程序码，永久有效，数量暂无限制
     *
     * @param page   小程序跳转页面，不填默认跳主页面，必须是已经发布的小程序存在的页面（否则报错）
     * @param scene  小程序场景值，保存跳转小程序用到的信息，最大32个可见字符，只支持数字，大小写英文以及部分特殊字符
     * @param config
     * @return
     */
    public byte[] getWxaCodeUnlimit(String page, String scene, WxProperties config) {
        String accessToken = getAccessToken(config);
        Map<String, Object> param = new HashMap<>(4);
        if (StringUtils.isNotBlank(scene)) {
            param.put(WxConfig.SCENE, scene);
        }
        if (StringUtils.isNotBlank(page)) {
            param.put(WxConfig.PAGE, page);
        }
        //二维码的宽度，单位 px，最小 280px，最大 1280px
        param.put(WxConfig.WIDTH, 430);
        //是否需要透明底色，为 true 时，生成透明底色的小程序
        param.put(WxConfig.IS_HYALINE, true);
        //设置accessToken
        Map<String, Object> paramsMap = new HashMap<>(1);
        paramsMap.put(WxConfig.ACCESS_TOKEN, accessToken);
        HttpEntity requestEntity = new HttpEntity(param, getHttpHeaders());
        byte[] result = null;
        try {
            result = restTemplate.postForEntity(buildGetUrl(WxConfig.QR_CODE_URI, paramsMap),
                    requestEntity, byte[].class).getBody();
        } catch (Exception e) {
            log.error("获取小程序码失败（不限次数）", e);
            throw new BusinessException("获取小程序码失败（不限次数）");
        }
        return result;
    }

    /**
     * 获取小程序二维码，适用于需要的码数量较少的业务场景。
     * 通过该接口生成的小程序码，永久有效，有数量限制，总共生成的码数量限制为 100,000
     *
     * @param path   扫码进入的小程序页面路径，最大长度 128 字节，不能为空
     * @param config
     * @return
     */
    public byte[] getCreateQRCode(String path, WxProperties config) {
        String accessToken = getAccessToken(config);
        log.info("获取accessToken为：{}", accessToken);
        Map<String, Object> param = new HashMap<>(3);
        param.put(WxConfig.PATH, path);
        //二维码的宽度，单位 px，最小 280px，最大 1280px
        param.put(WxConfig.WIDTH, 430);
        //是否需要透明底色，为 true 时，生成透明底色的小程序
        param.put(WxConfig.IS_HYALINE, true);
        //设置accessToken
        Map<String, Object> paramsMap = new HashMap<>(1);
        paramsMap.put(WxConfig.ACCESS_TOKEN, accessToken);
        HttpEntity requestEntity = new HttpEntity(param, getHttpHeaders());
        ResponseEntity<byte[]> result = restTemplate.postForEntity(buildGetUrl(WxConfig.GET_WXA_CODE, paramsMap),
                requestEntity, byte[].class);
        log.info("获取小程序码接口paramsMap:{},requestEntity:{}", paramsMap, requestEntity);
        return result.getBody();
    }

    /**
     * 将map转为url
     *
     * @param uri
     * @param map
     * @return
     */
    private String buildGetUrl(String uri, Map map) {
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append(uri).append("?");
        for (Iterator it = map.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = (Map.Entry) it.next();
            strBuilder.append(entry.getKey());
            strBuilder.append("=");
            strBuilder.append(entry.getValue());
            strBuilder.append("&");
        }
        String substring = strBuilder.toString().substring(0, strBuilder.length() - 1);
        log.info("获取小程序码接口获取url为：{}", substring);
        return substring;
    }

    /**
     * 设置请求头
     *
     * @return
     */
    private HttpHeaders getHttpHeaders() {
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);
        return requestHeaders;
    }
}
